#include "ShaderObject.hpp"

GLuint ShaderObject::enabled = 0;
ShaderObject* ShaderObject::baseShader = NULL;
ShaderObject* ShaderObject::baseColorShader = NULL;

ShaderObject::ShaderObject()
{
	m_program = glCreateProgram();
	m_vertexShader = 0;
	m_geometryShader = 0;
	m_fragmentShader = 0;
}

ShaderObject::ShaderObject(std::string filename)
{
	m_program = glCreateProgram();
	m_vertexShader = 0;
	m_geometryShader = 0;
	m_fragmentShader = 0;
    Load(filename);
}

ShaderObject::~ShaderObject()
{
	glDeleteShader(m_vertexShader);
	glDeleteShader(m_geometryShader);
	glDeleteShader(m_fragmentShader);
	glDeleteProgram(m_program);
}

/* static */ void ShaderObject::InitDefaultShaders()
{
  baseShader = new ShaderObject("./Resources/Shaders/basic");
  baseColorShader = new ShaderObject("./Resources/Shaders/basecolor");
}

ShaderObject* ShaderObject::GetBaseShader()
{
  return baseShader;
}

ShaderObject* ShaderObject::GetBaseColorShader()
{
  return baseColorShader;
}

void ShaderObject::BindAttributeLocations()
{
  glBindAttribLocation(m_program, Position,     "vPosition");
  glBindAttribLocation(m_program, Normal,       "vNormal");
  glBindAttribLocation(m_program, Texcoord,     "vTexCoord");
  glBindAttribLocation(m_program, Alpha,        "vAlpha");
  glBindAttribLocation(m_program, Material,     "vMaterial");
  glBindAttribLocation(m_program, Tangent,      "vTangent");
  glBindAttribLocation(m_program, Intensity,    "vIntensity");
  glBindAttribLocation(m_program, Color,        "vColor");
}

// Temporary method returning program id so we can use it to refer to shader 
// outside of ShaderObject class - until I port all the code I will be using
// it to not break existing one
GLuint ShaderObject::GetProgram()
{
    return m_program;    
}

GLuint ShaderObject::GetProgramId()
{
    return m_program;    
}

void ShaderObject::Reload()
{
  glDeleteShader(m_vertexShader);
  glDeleteShader(m_geometryShader);
  glDeleteShader(m_fragmentShader);	
  glDeleteProgram(m_program);
  m_program = glCreateProgram();
  uniformLocations.empty();
  Load(programName);
}

void ShaderObject::Enable()
{
    //if (enabled == m_program) {
        //std::cout << "Shader m_program already enabled!" << std::endl;
    //} else {
        ShaderObject::enabled = m_program;
        glUseProgram(m_program);
    //}
}

void ShaderObject::Disable()
{
  glBindTexture(GL_TEXTURE_2D, 0);
  glBindTexture(GL_TEXTURE_3D, 0);
  glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
  ShaderObject::enabled = 0;
  glUseProgram(0);
}

void ShaderObject::Load(std::string filename, bool loadGeometry)
{        
  programName = filename;
  m_vertexShader = LoadFromFile(filename + ".vert", GL_VERTEX_SHADER);
  m_fragmentShader = LoadFromFile(filename + ".frag", GL_FRAGMENT_SHADER);

  glAttachShader(m_program, m_vertexShader);
  if (loadGeometry)
  {
    m_geometryShader = LoadFromFile(filename + ".geom", GL_GEOMETRY_SHADER);
    if (m_geometryShader)
    {			
	    glAttachShader(m_program, m_geometryShader);
    }
  }

  glAttachShader(m_program, m_fragmentShader);        
      
  BindAttributeLocations();

  glLinkProgram(m_program);

  // For validation purposes
  //std::cout << "Linked " << filename << std::endl; 
  //std::cout << "===========================================" << std::endl;
  ShowInfoLog(m_program, glGetProgramiv, glGetProgramInfoLog);
}

void ShaderObject::SetUniform1i(std::string name, int i)
{
	GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
	    glUniform1i(loc, i);
	}
}

void ShaderObject::SetUniform1f(std::string name, float f)
{
	GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
		glUniform1f(loc, f);
	}
}

void ShaderObject::SetUniformVec3f(std::string name, Vec3f v)
{
	GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
	    glUniform3f(loc, v.x, v.y, v.z);
	}
}

void ShaderObject::SetUniformVec3d(std::string name, Vec3d v)
{
    GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
	    glUniform3f(loc, (float)v.x, (float)v.y, (float)v.z);
	}
}

void ShaderObject::SetUniformVec3i(std::string name, Vec3i v)
{
    GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
	    glUniform3i(loc, v.x, v.y, v.z);
	}
}

void ShaderObject::SetUniformMatrix4f(std::string name, const GLfloat *value)
{
	GLint loc = GetUniformLocation(name);
	if (loc != INACTIVE_UNIFORM) 
	{
		glUniformMatrix4fv(loc, 1, GL_FALSE, value);
	}
}

void ShaderObject::SetSampler2D(std::string name, GLuint unit, GLuint texture)
{
    // Set uniform to unit number
    SetUniform1i(name, unit);
    
    // Activate texture unit and bind texture
    glActiveTexture(GL_TEXTURE0 + unit);
    glBindTexture(GL_TEXTURE_2D, texture);
}

void ShaderObject::SetSampler3D(std::string name, GLuint unit, GLuint texture)
{
    // Set uniform to unit number
    SetUniform1i(name, unit);
    
    // Activate texture unit and bind texture
    glActiveTexture(GL_TEXTURE0 + unit);
    glBindTexture(GL_TEXTURE_3D, texture);
}

void ShaderObject::SetSampler2DArray(std::string name, GLuint unit, GLuint texture)
{
    // Set uniform to unit number
    SetUniform1i(name, unit);
    
    // Activate texture unit and bind texture
    glActiveTexture(GL_TEXTURE0 + unit);
    glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
}

// Cache results of glGetUniformLocation in a std::map structure, so we don't have to
// query them every time we set uniforms, but only once
GLint ShaderObject::GetUniformLocation(std::string uniform)
{
	std::map<std::string, GLint>::iterator it = uniformLocations.find(uniform);
	if(it == uniformLocations.end())
	{
		GLint location = glGetUniformLocation(m_program, uniform.c_str());
        //std::cout << "Shader " << programName << ": uniform=" << uniform.c_str() << ", location=" << location << std::endl;
		it = uniformLocations.insert(std::pair<std::string, GLint>(uniform, location)).first;
	}
	return it->second;
}

// Read shader source from a file
std::string ShaderObject::ReadShaderFile(std::string filename)
{
    std::string source = "";
    std::ifstream fileHandle(filename.c_str());
    if (fileHandle.is_open())
    {        
        std::string line;
        while (fileHandle.good())
        {
            getline(fileHandle, line);
            source.append(line + '\n');
        }
    }    
    fileHandle.close();
    return source;
}

void ShaderObject::ShowInfoLog(
    GLuint object,
    PFNGLGETSHADERIVPROC glGet__iv,
    PFNGLGETSHADERINFOLOGPROC glGet__InfoLog
)
{
    GLint log_length;
    char *log;

    glGet__iv(object, GL_INFO_LOG_LENGTH, &log_length);    
    log = (char *)malloc(log_length);
    glGet__InfoLog(object, log_length, NULL, log);
    fprintf(stderr, "%s", log);
    free(log);
}

// Main function to create shader object from source 
// (called by LoadShaderFromFile or in case we want to
// compile shader code on runtime)
GLuint ShaderObject::LoadFromMemory(const char *source, GLenum type)
{
    GLuint shader;
    GLint compiled;

    // Create the shader object of a given type
    shader = glCreateShader(type);
    
    if (shader == 0)
    {
        return 0;
    }

    // Load and compile shader source
    glShaderSource(shader, 1, &source, NULL);    
    glCompileShader(shader);

    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled)
    {
        ShowInfoLog(shader, glGetShaderiv, glGetShaderInfoLog);
        glDeleteShader(shader);
        return 0;
    }

    // Return shader
    return shader;
}

// Loads shader from a file - requires full path to resource
GLuint ShaderObject::LoadFromFile(std::string filename, GLenum type)
{
    std::string source = ReadShaderFile(filename.c_str());	
	if (source.empty())
	{
		std::cout << "Shader failed to load: " << filename << " (empty or non-existant)\n";
		return 0;
	}

    const char *s = source.c_str();    
    GLuint loadedShader = LoadFromMemory(s, type);
    if (loadedShader) 
    {     
        return loadedShader;
    } 
    else 
    {
        std::cout << "Shader failed to load: " << filename << '\n';
        return 0;
    }
}
